#include "voda.h"
#include "application.h"
#include "script.h"

namespace voda
{
	script::script (boost::shared_ptr<application> const & app)
		: app_ (app)
		, io_service_ (app->get_io_service())
		, strand_ (io_service_)
		, rt_ (app->get_runtime ())
		, cx_ (app->get_runtime ())
		, gl_ (app->get_runtime (), cx_)
	{
		script::lock const lock (this);

		JS_SetContextPrivate (cx_, this);	
		JS_SetErrorReporter (cx_, script::on_error);

		static JSFunctionSpec global_funcs [] =
		{
			JS_FS("include"			, script::Include		, 2, 0),
			JS_FS("Include"			, script::Include		, 2, 0),
			JS_FS("includeAsync"	, script::IncludeAsync	, 3, 0),
			JS_FS("IncludeAsync"	, script::IncludeAsync	, 3, 0),
			
			JS_FS("eval"			, script::Eval			, 2, 0),
			JS_FS("Eval"			, script::Eval			, 2, 0),
			JS_FS("evalAsync"		, script::EvalAsync		, 3, 0),
			JS_FS("EvalAsync"		, script::EvalAsync		, 3, 0),

			JS_FS("GC"				, script::GC			, 0, 0),
			JS_FS_END,
		};

		if (JS_TRUE != JS_DefineFunctions (cx_, gl_, global_funcs))
		{
			JS_ReportError (cx_, "script::JS_DefineFunctions failed!");
			return;
		}
	}

	boost::shared_ptr<script> script::create (boost::shared_ptr<application> const & app, boost::function<void(script*)> const & deleter)
	{
		return boost::shared_ptr<script> (new script (app), deleter);
	}

	script::~script (void)
	{
	}

	void script::on_error (JSContext *cx, char const * message, JSErrorReport *report)
	{
		script * pscript = reinterpret_cast<script *> (JS_GetContextPrivate (cx));
		if (!pscript)
		{
			return;
		}
	}

	void script::post (boost::function<void()> const & handler)
	{
		strand_.post (
			boost::bind(
				&script::call_handler, 
				shared_from_this(), 
				handler
			)
		);
	}

	void script::io_post (boost::function<void()> const & handler)
	{
		io_service_.post (
			boost::bind(
				&script::call_handler, 
				shared_from_this(), 
				handler
			)
		);
	}

	boost::asio::io_service & script::get_io_service ()
	{
		return io_service_;
	}

	boost::asio::io_service::strand & script::get_strand ()
	{
		return strand_;
	}

	boost::any script::include (std::string const & file, bool jit)
	{
		script::lock const lock (this);
		return unlocked_include (file, jit);
	}

	void script::include_async (std::string const & file, bool jit, boost::function<void(boost::any const &)> const & callback)
	{
		boost::function<boost::any()> const includefn = boost::bind (&script::include, shared_from_this(), file, jit);
		boost::function<void()> const cbfn = boost::bind (callback, includefn);
		post (cbfn);
	}

	boost::any script::eval (std::string const & text, bool jit)
	{
		script::lock const lock (this);
		return unlocked_eval (text, jit);
	}

	void script::eval_async (std::string const & text, bool jit, boost::function<void(boost::any const &)> const & callback)
	{
		boost::function<boost::any()> const evalfn = boost::bind (
				&script::eval, 
				shared_from_this(), 
				text, 
				jit
			);

		boost::function<void()> const cbfn = boost::bind (
				callback, 
				evalfn
			);

		post (cbfn);
	}

	JSBool script::Include (JSContext *cx, unsigned argc, jsval *vp)
	{
		//	Check the script context
		script * pscript = reinterpret_cast<script *> (JS_GetContextPrivate (cx));
		if (!pscript)
		{
			JS_ReportError (cx, "include: private context was not set!");
			return JS_FALSE;
		}

		//	Get the parameters
		jsval * const argv = JS_ARGV(cx, vp);
		JSString * jstrPath = NULL;
		JSBool bJit = JS_FALSE;
		if (!JS_ConvertArguments (cx, argc, argv, "S/b", &jstrPath, &bJit))
		{
			JS_ReportError (cx, "include: argument must be (path [,jit=false])!");
			return JS_FALSE;
		}

		//	Convert the path to UTF8 string
		JSAutoByteString jsstr;
		char const * path = jsstr.encodeUtf8 (cx, jstrPath);
		if (NULL == path)
		{
			return JS_FALSE;
		}

		//	Call exec
		boost::any retVal = pscript->unlocked_include (path, (bJit ? true : false));
		jsval * pJSVal = boost::any_cast<jsval> (&retVal);
		if (pJSVal) 
		{
			JS_SET_RVAL (cx, vp, *pJSVal);
			return JS_TRUE;
		}
		else
		{
			std::exception * pex = boost::any_cast<std::exception>(&retVal);
			if (pex) 
			{
				JS_ReportError (cx, "Include file (%s) throw an excepton exception: %s", "file", pex->what());
			}
			return JS_FALSE;
		}
	}

	JSBool script::IncludeAsync (JSContext *cx, unsigned argc, jsval *vp)
	{
		//	Check the script context
		script * pscript = reinterpret_cast<script *> (JS_GetContextPrivate (cx));
		if (!pscript)
		{
			JS_ReportError (cx, "IncludeAsync: private context was not set!");
			return JS_FALSE;
		}

		//	Get the parameters
		jsval * const argv = JS_ARGV(cx, vp);
		JSString * pPath = NULL;
		JSBool bJit = JS_FALSE;
		JSObject * pCallback = NULL;
		if (!JS_ConvertArguments (cx, argc, argv, "Sbo", &pPath, &bJit, &pCallback))
		{
			JS_ReportError (cx, "IncludeAsync: argument must be (path, jit, function(res){})!");
			return JS_FALSE;
		}

		JS::Heap<JSString*> strHeap (pPath);
		JS::Heap<JSObject*> objHeap (pCallback);
//		boost::function<void()> postOnIncludeAsync = boost::bind(&script::on_include_async, pscript->shared_from_this(), cx, strHeap, bJit, objHeap);
//		pscript->post (postOnIncludeAsync);

		JS_SET_RVAL(cx, vp, JSVAL_VOID);
		return JS_TRUE;
	}

	JSBool script::Eval (JSContext *cx, unsigned argc, jsval *vp)
	{
		//	Check the script context
		script * pscript = reinterpret_cast<script *> (JS_GetContextPrivate (cx));
		if (!pscript)
		{
			JS_ReportError (cx, "eval: private context was not set!");
			return JS_FALSE;
		}

		//	Get the parameters
		jsval * const argv = JS_ARGV(cx, vp);
		JSString* jstrScript = NULL;
		JSBool bJit = JS_FALSE;
		if (!JS_ConvertArguments (cx, argc, argv, "S/b", &jstrScript, &bJit))
		{
			JS_ReportError (cx, "eval: argument must be (text [,jit=false])!");
			return JS_FALSE;
		}

		//	Convert the path to UTF8 string
		JSAutoByteString jsstr;
		char const * text = jsstr.encodeUtf8 (cx, jstrScript);
		if (NULL == text)
		{
			return JS_FALSE;
		}

		//	Call eval
		boost::any retVal = pscript->unlocked_eval (text, (bJit ? true : false));
		jsval * pJSVal = boost::any_cast<jsval> (&retVal);
		if (pJSVal) 
		{
			JS_SET_RVAL (cx, vp, *pJSVal);
			return JS_TRUE;
		}
		else
		{
			std::exception * pex = boost::any_cast<std::exception>(&retVal);
			if (pex) 
			{
				JS_ReportError (cx, "Eval throw an excepton exception: %s", pex->what());
			}
			return JS_FALSE;
		}
	}

	JSBool script::EvalAsync (JSContext *cx, unsigned argc, jsval *vp)
	{
		//	Check the script context
		script * pscript = reinterpret_cast<script *> (JS_GetContextPrivate (cx));
		if (!pscript)
		{
			JS_ReportError (cx, "EvalAsync: private context was not set!");
			return JS_FALSE;
		}

		//	Get the parameters
		jsval * const argv = JS_ARGV(cx, vp);
		JSString* pText = NULL;
		JSBool bJit = JS_FALSE;
		JSObject* pCallback = NULL;
		if (!JS_ConvertArguments (cx, argc, argv, "Sbo", &pText, &bJit, &pCallback))
		{
			JS_ReportError (cx, "EvalAsync: argument must be (text, jit, function(res){})!");
			return JS_FALSE;
		}

		JS::Heap<JSString*> strHeap (pText);
		JS::Heap<JSObject*> objHeap (pCallback);
		
//		boost::function<void()> postOnEvalAsync = boost::bind(&script::on_eval_async, pscript->shared_from_this(), cx, strHeap, bJit, objHeap);
//		pscript->post (postOnEvalAsync);

		JS_SET_RVAL(cx, vp, JSVAL_VOID);
		return JS_TRUE;
	}
	
	JSBool script::GC (JSContext *cx, unsigned /*argc*/, jsval *vp)
	{
		JS_GC (JS_GetRuntime (cx));
		JS_SET_RVAL(cx, vp, JSVAL_VOID);
		return JS_TRUE;
	}

	void script::call_handler (boost::function<void()> const & fn)
	{
		if (fn) {
			fn ();
		}
	}

	boost::any script::unlocked_include (std::string const & file, bool jit)
	{
		try 
		{
			JS::CompileOptions opts (cx_);
			opts.setUTF8 (true);
			opts.setCompileAndGo (jit);
			opts.setNoScriptRval (false);

			JS::RootedObject global (cx_, gl_);
			jsval retVal;
			if (JS::Evaluate (cx_, global, opts, file.c_str(), &retVal))
			{
				return boost::any (retVal);
			}
			else
			{
				return boost::any (std::exception ("Script failed."));
			}
		}
		catch (std::exception & ex)
		{
			return boost::any (ex);
		}
	}

	boost::any script::unlocked_eval (std::string const & text, bool jit)
	{
		try
		{
			JS::CompileOptions opts (cx_);
			opts.setUTF8 (true);
			opts.setCompileAndGo (jit);
			opts.setNoScriptRval (false);

			JS::RootedObject global (cx_, gl_);

			jsval retVal;
			if (JS::Evaluate (cx_, global, opts, text.c_str(), text.length(), &retVal))
			{
				return boost::any (retVal);
			}
			else
			{
				return boost::any (std::exception ("Script eval failed!"));
			}
		}
		catch (std::exception & ex)
		{
			return boost::any (ex);
		}
	}

	void script::on_include_async(JSContext *cx, JS::Heap<JSString*> & path, JSBool jit, JS::Heap<JSObject*> & callback)
	{
		script::lock const lock (this);

		JSAutoByteString jsstr;
		char const * ppath = jsstr.encodeUtf8 (cx, path);
		if (NULL == ppath)
		{
			return;
		}

		jsval argv [] = 
		{
			JSVAL_NULL,
			JSVAL_NULL,
		};

		boost::any res = unlocked_include (ppath, jit ? true : false);
		jsval * pRes = boost::any_cast<jsval> (&res);
		if (NULL != pRes) 
		{
			argv[0] = *pRes;
			argv[1] = JSVAL_NULL;
		}

		std::exception *pEx = boost::any_cast<std::exception> (&res);
		if (pEx)
		{
			argv[0] = JSVAL_NULL;
			argv[1] = JSVAL_NULL;
		}

		jsval RetVal;
		if (JS::Call (cx, JSVAL_NULL, callback, 2, argv, &RetVal))
		{
		}
		else
		{
		}
	}

	void script::on_eval_async(JSContext *cx, JS::Heap<JSString*> & text, JSBool jit, JS::Heap<JSObject*> & callback)
	{
		script::lock const lock (this);

		JSAutoByteString jsstr;
		char const * ptext = jsstr.encodeUtf8 (cx, text);
		if (NULL == ptext)
		{
			return;
		}

		jsval argv [] = 
		{
			JSVAL_NULL,
			JSVAL_NULL
		};

		boost::any res = unlocked_eval (ptext, jit ? true : false);
		jsval * pRes = boost::any_cast<jsval> (&res);
		if (NULL != pRes) 
		{
			argv[0] = *pRes;
			argv[1] = JSVAL_NULL;
		}

		std::exception *pEx = boost::any_cast<std::exception> (&res);
		if (pEx)
		{
			argv[0] = JSVAL_NULL;
			argv[1] = JSVAL_NULL;
		}

		jsval RetVal;
		if (JS::Call (cx, JSVAL_NULL, callback, 2, argv, &RetVal))
		{
		}
		else
		{
		}
	}
}